package sbt.metamodel;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;

/**
 * I represent a transformed trace.
 * @author pa
 *
 */
public class TransformedTrace
extends AbstractTrace implements TraceListener {
	
	private String model, origin;
	private TransformationMethod method;
	private Set<AbstractTrace> sources;
	private Map<String, Object> parameters;
	
	/**
	 * Construct a new transformed trace.
	 * 
	 * @param method  the transformation to use
	 * @param sources  the source traces
	 * @param parameters  the parameters (see {@link AbstractTrace.Attr})
	 */
	public TransformedTrace(TransformationMethod method,
			Set<AbstractTrace> sources, Map<String, Object> parameters) {
		model = null;
		origin = null;
		if (sources.size() == 0) {
			throw new MetamodelException("No source");
		}
        this.sources = new HashSet<AbstractTrace>(sources);
        for (AbstractTrace s: sources) {
        	s.register(this);
        }
		this.parameters = new HashMap<String, Object>(parameters);
		this.method = method;
		method.init(this);
	}

	/**
	 * Convenience constructor accepting a single source.
	 * @see #TransformedTrace(TransformationMethod, Set, Map)
	 */
	public TransformedTrace(TransformationMethod method,
			AbstractTrace source, Map<String, Object>parameters) {
    	this(method, Collections.singleton(source), parameters);
	}

	@Override
	public String getModel() {
		if (model == null) {
			model = method.computeModel();
		}
		return model;
	}
	
	@Override
	public String getOrigin() {
		if (origin == null) {
			origin = method.computeOrigin();
		}
		return origin;
	}
	
	@Override
	public SortedMap<Long, Obsel> getObselsById() {
		if (!isUpToDate()) {
			method.updateObsels();
		}
		return super.getObselsById();
	}
	
	@Override
	public SortedMap<Long, Obsel> getObselsByBegin() {
		if (!isUpToDate()) {
			method.updateObsels();
		}
		return super.getObselsByBegin();
	}
	
	@Override
	public SortedMap<Long, Obsel> getObselsByEnd() {
		if (!isUpToDate()) {
			method.updateObsels();
		}
		return super.getObselsByEnd();
	}

	/**
	 * The method used by this transformed trace.
	 * @return the transformation method
	 */
	public TransformationMethod getMethod() {
		return method;
	}
	
	/**
	 * The source traces of this transformed trace.
	 * @return  the source traces
	 */
	public Set<AbstractTrace> getSources() {
		return Collections.unmodifiableSet(sources);
	}
	
	/**
	 * Add a source trace to this transformed trace.
	 * <p>
	 * NB: has no effect if source is already a source of this trace.
	 * @param source  the source to add
	 */
	public void addSource(AbstractTrace source) {
		sources.add(source);
		method.notifyMetadataChange();
	}
	
	/**
	 * Remove a source trace from this transformed trace.
	 * <p>
	 * NB: has no effect if source is not a source of this trace.
	 * @param source  the source to add
	 */
	public void removeSource(AbstractTrace source) {
		sources.remove(source);
		method.notifyMetadataChange();
	}

	/**
	 * Replace a source of this transformed trace by another trace.
	 * <p>
	 * This amounts to {@link #removeSource(AbstractTrace)} old, then to
	 * {@link #addSource(AbstractTrace)} nevv, but prevents an error if the
	 * underlying method only accepts a fixed number of sources.
	 * 
	 * @param old  the source trace to remove
	 * @param nevv  the source trace to add
	 */
	public void replaceSource(AbstractTrace old, AbstractTrace nevv) {
		sources.remove(old);
		sources.add(nevv);
		method.notifyMetadataChange();
	}
	
	/**
	 * The parameters of this transformed trace as a read-only map.
	 * @return  the parameters
	 * @see #getParameter(String)
	 * @see #setParameter(String, Object)
	 * @see #unsetParameter(String)
	 */
	public Map<String, Object> getParameters() {
		return Collections.unmodifiableMap(parameters);
	}
	
	/**
	 * Retrieve a parameter value.
	 * @param name  the name of the parameter to get
	 * @return  the value of the parameter
	 * @see #getParameters()
	 */
	public Object getParameter(String name) {
		return parameters.get(name);
	}
	
	/**
	 * Set a parameter value.
	 * @param name  the name of the parameter to get
	 * @param value  the value to set to that parameter
	 * @see #getParameters()
	 * @see #getParameter(String)
	 * @see #unsetParameter(String)
	 */
	public void setParameter(String name, Object value) {
		clearObsels();
		clearMetadata();
		parameters.put(name, value);
		method.notifyMetadataChange();
	}
	
	/**
	 * Unset a parameter.
	 * @param name  the name of the parameter to unset
	 * @see #getParameters()
	 * @see #getParameter(String)
	 * @see #setParameter(String, Object)
	 */
	public void unsetParameter(String name) {
		clearObsels();
		clearMetadata();
		parameters.remove(name);
		method.notifyMetadataChange();
	}
	
	/**
	 * Is this transformed trace up to date w.r.t. its sources?
	 * @return whether this transformed trace is up to date
	 */
	public boolean isUpToDate() {
		if (method.hasPendingChanges()) {
			return false;
		}
		for (AbstractTrace t: sources) {
			if (t instanceof TransformedTrace &&
					!((TransformedTrace) t).isUpToDate()) {
				return false;
			}
		}
		return true;
	}
	
	/**
	 * Implements {@link TraceListener#notifyMetadataChange(AbstractTrace)}
	 */
	public void notifyMetadataChange(AbstractTrace trace) {
		clearMetadata();
	}

	/**
	 * Implements {@link TraceListener#notifyObselsChange(AbstractTrace, sbt.metamodel.TraceListener.ChangeType)}
	 */
	public void notifyObselsChange(AbstractTrace trace, ChangeType type) {
		method.notifyObselsChange(trace, type);
	}
	
	/**
	 * Reset the computed metadata (model and origin) of this transformed
	 * trace.
	 */
	protected void clearMetadata() {
		model = null;
		origin = null;
		notifyAllMetadataChange();
	}
}
